/**
 * @file jsoneditor.js
 *
 * @brief
 * JSONEditor is a web-based tool to view, edit, and format JSON.
 * It shows data a clear, editable treeview.
 *
 * Supported browsers: Chrome, Firefox, Safari, Opera, Internet Explorer 8+
 *
 * @license
 * This json editor is open sourced with the intention to use the editor as
 * a component in your own application. Not to just copy and monetize the editor
 * as it is.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy
 * of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 *
 * Copyright (c) 2011-2012 Jos de Jong, http://jsoneditoronline.org
 *
 * @author  Jos de Jong, <wjosdejong@gmail.com>
 * @date    2012-12-08
 */
// Internet Explorer 8 and older does not support Array.indexOf,
// so we define it here in that case
// http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/
if (!Array.prototype.indexOf) {
  Array.prototype.indexOf = function(obj) {
    for (var i = 0; i < this.length; i++) {
      if (this[i] == obj) {
        return i;
      }
    }
    return -1;
  };
}

// Internet Explorer 8 and older does not support Array.forEach,
// so we define it here in that case
// https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/forEach
if (!Array.prototype.forEach) {
  Array.prototype.forEach = function(fn, scope) {
    for (var i = 0, len = this.length; i < len; ++i) {
      fn.call(scope || this, this[i], i, this);
    }
  };
}

// define variable JSON, needed for correct error handling on IE7 and older
var JSON;

/**
 * JSONEditor
 * @param {Element} container    Container element
 * @param {Object}  [options]    Object with options. available options:
 *                               {String} mode      Editor mode. Available values:
 *                                                  'editor' (default), 'viewer'.
 *                               {Boolean} search   Enable search box.
 *                                                  True by default
 *                               {Boolean} history  Enable history (undo/redo).
 *                                                  True by default
 *                               {function} change  Callback method, triggered
 *                                                  on change of contents
 *                               {String} name      Field name for the root node.
 * @param {Object | undefined} json JSON object
 */
JSONEditor = function(container, options, json) {
  // check availability of JSON parser (not available in IE7 and older)
  if (!JSON) {
    throw new Error(
      "您当前使用的浏览器不支持 JSON. \n\n" +
        "请下载安装最新版本的浏览器, 本站推荐Google Chrome.\n" +
        "(PS: 当前主流浏览器都支持JSON)."
    );
  }

  if (!container) {
    throw new Error("没有提供容器元素.");
  }
  this.container = container;
  this.dom = {};

  this._setOptions(options);

  if (this.options.history && this.editable) {
    this.history = new JSONEditor.History(this);
  }

  this._createFrame();
  this._createTable();

  this.set(json || {});
};

/**
 * Initialize and set default options
 * @param {Object}  [options]    Object with options. available options:
 *                               {String} mode      Editor mode. Available values:
 *                                                  'editor' (default), 'viewer'.
 *                               {Boolean} search   Enable search box.
 *                                                  True by default.
 *                               {Boolean} history  Enable history (undo/redo).
 *                                                  True by default.
 *                               {function} change  Callback method, triggered
 *                                                  on change of contents.
 *                               {String} name      Field name for the root node.
 * @private
 */
JSONEditor.prototype._setOptions = function(options) {
  this.options = {
    search: true,
    history: true,
    mode: "editor",
    name: undefined // field name of root node
  };

  // copy all options
  if (options) {
    for (var prop in options) {
      if (options.hasOwnProperty(prop)) {
        this.options[prop] = options[prop];
      }
    }

    // check for deprecated options
    if (options.enableSearch) {
      // deprecated since version 1.6.0, 2012-11-03
      this.options.search = options.enableSearch;
      // console.log('WARNING: Option "enableSearch" is deprecated. Use "search" instead.');
    }
    if (options.enableHistory) {
      // deprecated since version 1.6.0, 2012-11-03
      this.options.search = options.enableHistory;
      // console.log('WARNING: Option "enableHistory" is deprecated. Use "history" instead.');
    }
  }

  // interpret the options
  this.editable = this.options.mode != "viewer";
};

// node currently being edited
JSONEditor.focusNode = undefined;

/**
 * Set JSON object in editor
 * @param {Object | undefined} json      JSON data
 * @param {String}             [name]    Optional field name for the root node.
 *                                       Can also be set using setName(name).
 */
JSONEditor.prototype.set = function(json, name) {
  // adjust field name for root node
  if (name) {
    this.options.name = name;
  }

  // verify if json is valid JSON, ignore when a function
  if (json instanceof Function || json === undefined) {
    this.clear();
  } else {
    this.content.removeChild(this.table); // Take the table offline

    // replace the root node
    var params = {
      field: this.options.name,
      value: json
    };
    var node = new JSONEditor.Node(this, params);
    this._setRoot(node);

    // expand
    var recurse = false;
    this.node.expand(recurse);

    this.content.appendChild(this.table); // Put the table online again
  }

  // TODO: maintain history, store last state and previous document
  if (this.history) {
    this.history.clear();
  }
};

/**
 * Get JSON object from editor
 * @return {Object | undefined} json
 */
JSONEditor.prototype.get = function() {
  // remove focus from currently edited node
  if (JSONEditor.focusNode) {
    JSONEditor.focusNode.blur();
  }

  if (this.node) {
    return this.node.getValue();
  } else {
    return undefined;
  }
};

/**
 * Set a field name for the root node.
 * @param {String | undefined} name
 */
JSONEditor.prototype.setName = function(name) {
  this.options.name = name;
  if (this.node) {
    this.node.updateField(this.options.name);
  }
};

/**
 * Get the field name for the root node.
 * @return {String | undefined} name
 */
JSONEditor.prototype.getName = function() {
  return this.options.name;
};

/**
 * Remove the root node from the editor
 */
JSONEditor.prototype.clear = function() {
  if (this.node) {
    this.node.collapse();
    this.tbody.removeChild(this.node.getDom());
    delete this.node;
  }
};

/**
 * Set the root node for the json editor
 * @param {JSONEditor.Node} node
 * @private
 */
JSONEditor.prototype._setRoot = function(node) {
  this.clear();

  this.node = node;

  // append to the dom
  this.tbody.appendChild(node.getDom());
};

/**
 * Search text in all nodes
 * The nodes will be expanded when the text is found one of its childs,
 * else it will be collapsed. Searches are case insensitive.
 * @param {String} text
 * @return {Object[]} results  Array with nodes containing the search results
 *                             The result objects contains fields:
 *                             - {JSONEditor.Node} node,
 *                             - {String} elem  the dom element name where
 *                                              the result is found ('field' or
 *                                              'value')
 */
JSONEditor.prototype.search = function(text) {
  var results;
  if (this.node) {
    this.content.removeChild(this.table); // Take the table offline
    results = this.node.search(text);
    this.content.appendChild(this.table); // Put the table online again
  } else {
    results = [];
  }

  return results;
};

/**
 * Expand all nodes
 */
JSONEditor.prototype.expandAll = function() {
  if (this.node) {
    this.content.removeChild(this.table); // Take the table offline
    this.node.expand();
    this.content.appendChild(this.table); // Put the table online again
  }
};

/**
 * Collapse all nodes
 */
JSONEditor.prototype.collapseAll = function() {
  if (this.node) {
    this.content.removeChild(this.table); // Take the table offline
    this.node.collapse();
    this.content.appendChild(this.table); // Put the table online again
  }
};

/**
 * The method onChange is called whenever a field or value is changed, created,
 * deleted, duplicated, etc.
 * @param {String} action  Change action. Available values: "editField",
 *                         "editValue", "changeType", "appendNode",
 *                         "removeNode", "duplicateNode", "moveNode", "expand",
 *                         "collapse".
 * @param {Object} params  Object containing parameters describing the change.
 *                         The parameters in params depend on the action (for
 *                         example for "editValue" the Node, old value, and new
 *                         value are provided). params contains all information
 *                         needed to undo or redo the action.
 */
JSONEditor.prototype.onAction = function(action, params) {
  // add an action to the history
  if (this.history) {
    this.history.add(action, params);
  }

  // trigger the onChange callback
  if (this.options.change) {
    try {
      this.options.change();
    } catch (err) {
      //console.log('Error in change callback: ', err);
    }
  }
};

/**
 * Set the focus to the JSONEditor. A hidden input field will be created
 * which captures key events
 */
// TODO: use the focus method?
JSONEditor.prototype.focus = function() {
  /*
    if (!this.dom.focus) {
        this.dom.focus = document.createElement('input');
        this.dom.focus.className = 'jsoneditor-hidden-focus';

        var editor = this;
        this.dom.focus.onblur = function () {
            // remove itself
            if (editor.dom.focus) {
                var focus = editor.dom.focus;
                delete editor.dom.focus;
                editor.frame.removeChild(focus);
            }
        };

        // attach the hidden input box to the DOM
        if (this.frame.firstChild) {
            this.frame.insertBefore(this.dom.focus, this.frame.firstChild);
        }
        else {
            this.frame.appendChild(this.dom.focus);
        }
    }
    this.dom.focus.focus();
    */
};

/**
 * Adjust the scroll position such that given top position is shown at 1/4
 * of the window height.
 * @param {Number} top
 */
JSONEditor.prototype.scrollTo = function(top) {
  var content = this.content;
  if (content) {
    // cancel any running animation
    var editor = this;
    if (editor.animateTimeout) {
      clearTimeout(editor.animateTimeout);
      delete editor.animateTimeout;
    }

    // calculate final scroll position
    var height = content.clientHeight;
    var bottom = content.scrollHeight - height;
    var finalScrollTop = Math.min(Math.max(top - height / 4, 0), bottom);

    // animate towards the new scroll position
    var animate = function() {
      var scrollTop = content.scrollTop;
      var diff = finalScrollTop - scrollTop;
      if (Math.abs(diff) > 3) {
        content.scrollTop += diff / 3;
        editor.animateTimeout = setTimeout(animate, 50);
      }
    };
    animate();
  }
};

/**
 * @constructor JSONEditor.History
 * Store action history, enables undo and redo
 * @param {JSONEditor} editor
 */
JSONEditor.History = function(editor) {
  this.editor = editor;
  this.clear();

  // map with all supported actions
  this.actions = {
    editField: {
      undo: function(obj) {
        obj.params.node.updateField(obj.params.oldValue);
      },
      redo: function(obj) {
        obj.params.node.updateField(obj.params.newValue);
      }
    },
    editValue: {
      undo: function(obj) {
        obj.params.node.updateValue(obj.params.oldValue);
      },
      redo: function(obj) {
        obj.params.node.updateValue(obj.params.newValue);
      }
    },
    appendNode: {
      undo: function(obj) {
        obj.params.parent.removeChild(obj.params.node);
      },
      redo: function(obj) {
        obj.params.parent.appendChild(obj.params.node);
      }
    },
    removeNode: {
      undo: function(obj) {
        var parent = obj.params.parent;
        var beforeNode = parent.childs[obj.params.index] || parent.append;
        parent.insertBefore(obj.params.node, beforeNode);
      },
      redo: function(obj) {
        obj.params.parent.removeChild(obj.params.node);
      }
    },
    duplicateNode: {
      undo: function(obj) {
        obj.params.parent.removeChild(obj.params.clone);
      },
      redo: function(obj) {
        // TODO: insert after instead of insert before
        obj.params.parent.insertBefore(obj.params.clone, obj.params.node);
      }
    },
    changeType: {
      undo: function(obj) {
        obj.params.node.changeType(obj.params.oldType);
      },
      redo: function(obj) {
        obj.params.node.changeType(obj.params.newType);
      }
    },
    moveNode: {
      undo: function(obj) {
        obj.params.startParent.moveTo(obj.params.node, obj.params.startIndex);
      },
      redo: function(obj) {
        obj.params.endParent.moveTo(obj.params.node, obj.params.endIndex);
      }
    }

    // TODO: restore the original caret position and selection with each undo
    // TODO: implement history for actions "expand", "collapse", "scroll", "setDocument"
  };
};

/**
 * The method onChange is executed when the History is changed, and can
 * be overloaded.
 */
JSONEditor.History.prototype.onChange = function() {};

/**
 * Add a new action to the history
 * @param {String} action  The executed action. Available actions: "editField",
 *                         "editValue", "changeType", "appendNode",
 *                         "removeNode", "duplicateNode", "moveNode"
 * @param {Object} params  Object containing parameters describing the change.
 *                         The parameters in params depend on the action (for
 *                         example for "editValue" the Node, old value, and new
 *                         value are provided). params contains all information
 *                         needed to undo or redo the action.
 */
JSONEditor.History.prototype.add = function(action, params) {
  this.index++;
  this.history[this.index] = {
    action: action,
    params: params,
    timestamp: new Date()
  };

  // remove redo actions which are invalid now
  if (this.index < this.history.length - 1) {
    this.history.splice(this.index + 1, this.history.length - this.index - 1);
  }

  // fire onchange event
  this.onChange();
};

/**
 * Clear history
 */
JSONEditor.History.prototype.clear = function() {
  this.history = [];
  this.index = -1;

  // fire onchange event
  this.onChange();
};

/**
 * Check if there is an action available for undo
 * @return {Boolean} canUndo
 */
JSONEditor.History.prototype.canUndo = function() {
  return this.index >= 0;
};

/**
 * Check if there is an action available for redo
 * @return {Boolean} canRedo
 */
JSONEditor.History.prototype.canRedo = function() {
  return this.index < this.history.length - 1;
};

/**
 * Undo the last action
 */
JSONEditor.History.prototype.undo = function() {
  if (this.canUndo()) {
    var obj = this.history[this.index];
    if (obj) {
      var action = this.actions[obj.action];
      if (action && action.undo) {
        action.undo(obj);
      } else {
        //console.log('Error: unknown action "' + obj.action + '"');
      }
    }
    this.index--;

    // fire onchange event
    this.onChange();
  }
};

/**
 * Redo the last action
 */
JSONEditor.History.prototype.redo = function() {
  if (this.canRedo()) {
    this.index++;

    var obj = this.history[this.index];
    if (obj) {
      if (obj) {
        var action = this.actions[obj.action];
        if (action && action.redo) {
          action.redo(obj);
        } else {
          //console.log('Error: unknown action "' + obj.action + '"');
        }
      }
    }

    // fire onchange event
    this.onChange();
  }
};

/**
 * @constructor JSONEditor.Node
 * Create a new Node
 * @param {JSONEditor} editor
 * @param {Object} params   Can contain parameters: field, fieldEditable, value.
 */
JSONEditor.Node = function(editor, params) {
  this.editor = editor;
  this.dom = {};
  this.expanded = false;

  if (params && params instanceof Object) {
    this.setField(params.field, params.fieldEditable);
    this.setValue(params.value);
  } else {
    this.setField();
    this.setValue();
  }
};

/**
 * Set parent node
 * @param {JSONEditor.Node} parent
 */
JSONEditor.Node.prototype.setParent = function(parent) {
  this.parent = parent;
};

/**
 * Get parent node. Returns undefined when no parent node is set.
 * @return {JSONEditor.Node} parent
 */
JSONEditor.Node.prototype.getParent = function() {
  return this.parent;
};

/**
 * Set field
 * @param {String} field
 * @param {boolean} fieldEditable
 */
JSONEditor.Node.prototype.setField = function(field, fieldEditable) {
  this.field = field;
  this.fieldEditable = fieldEditable == true;
};

/**
 * Get field
 * @return {String}
 */
JSONEditor.Node.prototype.getField = function() {
  if (this.field === undefined) {
    this._getDomField();
  }

  return this.field;
};

/**
 * Set value. Value is a JSON structure or an element String, Boolean, etc.
 * @param {*} value
 */
JSONEditor.Node.prototype.setValue = function(value) {
  var childValue, child;

  // first clear all current childs (if any)
  var childs = this.childs;
  if (childs) {
    while (childs.length) {
      this.removeChild(childs[0]);
    }
  }

  // TODO: remove the DOM of this Node

  this.type = this._getType(value);
  if (this.type == "array") {
    // array
    this.childs = [];
    for (var i = 0, iMax = value.length; i < iMax; i++) {
      childValue = value[i];
      if (childValue !== undefined && !(childValue instanceof Function)) {
        // ignore undefined and functions
        child = new JSONEditor.Node(this.editor, {
          value: childValue
        });
        this.appendChild(child);
      }
    }
    this.value = "";
  } else if (this.type == "object") {
    // object
    this.childs = [];
    for (var childField in value) {
      if (value.hasOwnProperty(childField)) {
        childValue = value[childField];
        if (childValue !== undefined && !(childValue instanceof Function)) {
          // ignore undefined and functions
          child = new JSONEditor.Node(this.editor, {
            field: childField,
            value: childValue
          });
          this.appendChild(child);
        }
      }
    }
    this.value = "";
  } else {
    // value
    this.childs = undefined;
    this.value = value;
    /* TODO
         if (typeof(value) == 'string') {
         var escValue = JSON.stringify(value);
         this.value = escValue.substring(1, escValue.length - 1);
         console.log('check', value, this.value);
         }
         else {
         this.value = value;
         }
         */
  }
};

/**
 * Get value. Value is a JSON structure
 * @return {*} value
 */
JSONEditor.Node.prototype.getValue = function() {
  //var childs, i, iMax;

  if (this.type == "array") {
    var arr = [];
    this.childs.forEach(function(child) {
      arr.push(child.getValue());
    });
    return arr;
  } else if (this.type == "object") {
    var obj = {};
    this.childs.forEach(function(child) {
      obj[child.getField()] = child.getValue();
    });
    return obj;
  } else {
    if (this.value === undefined) {
      this._getDomValue();
    }

    return this.value;
  }
};

/**
 * Get the nesting level of this node
 * @return {Number} level
 */
JSONEditor.Node.prototype.getLevel = function() {
  return this.parent ? this.parent.getLevel() + 1 : 0;
};

/**
 * Create a clone of a node
 * The complete state of a clone is copied, including whether it is expanded or
 * not. The DOM elements are not cloned.
 * @return {JSONEditor.Node} clone
 */
JSONEditor.Node.prototype.clone = function() {
  var clone = new JSONEditor.Node(this.editor);
  clone.type = this.type;
  clone.field = this.field;
  clone.fieldInnerText = this.fieldInnerText;
  clone.fieldEditable = this.fieldEditable;
  clone.value = this.value;
  clone.valueInnerText = this.valueInnerText;
  clone.expanded = this.expanded;

  if (this.childs) {
    // an object or array
    var cloneChilds = [];
    this.childs.forEach(function(child) {
      var childClone = child.clone();
      childClone.setParent(clone);
      cloneChilds.push(childClone);
    });
    clone.childs = cloneChilds;
  } else {
    // a value
    clone.childs = undefined;
  }

  return clone;
};

/**
 * Expand this node and optionally its childs.
 * @param {boolean} recurse   Optional recursion, true by default. When
 *                            true, all childs will be expanded recursively
 */
JSONEditor.Node.prototype.expand = function(recurse) {
  if (!this.childs) {
    return;
  }

  // set this node expanded
  this.expanded = true;
  if (this.dom.expand) {
    this.dom.expand.className = "jsoneditor-expanded";
  }

  this.showChilds();

  if (recurse != false) {
    this.childs.forEach(function(child) {
      child.expand(recurse);
    });
  }
};

/**
 * Collapse this node and optionally its childs.
 * @param {Number} recurse   Optional recursion, true by default. When
 *                            true, all childs will be collapsed recursively
 */
JSONEditor.Node.prototype.collapse = function(recurse) {
  if (!this.childs) {
    return;
  }

  this.hideChilds();

  // collapse childs in case of recurse
  if (recurse != false) {
    this.childs.forEach(function(child) {
      child.collapse(recurse);
    });
  }

  // make this node collapsed
  if (this.dom.expand) {
    this.dom.expand.className = "jsoneditor-collapsed";
  }
  this.expanded = false;
};

/**
 * Recursively show all childs when they are expanded
 */
JSONEditor.Node.prototype.showChilds = function() {
  var childs = this.childs;
  if (!childs) {
    return;
  }
  if (!this.expanded) {
    return;
  }

  var tr = this.dom.tr;
  var table = tr ? tr.parentNode : undefined;
  if (table) {
    // show row with append button
    var append = this.getAppend();
    var nextTr = tr.nextSibling;
    if (nextTr) {
      table.insertBefore(append, nextTr);
    } else {
      table.appendChild(append);
    }

    // show childs
    this.childs.forEach(function(child) {
      table.insertBefore(child.getDom(), append);
      child.showChilds();
    });
  }
};

/**
 * Hide the node with all its childs
 */
JSONEditor.Node.prototype.hide = function() {
  var tr = this.dom.tr;
  var table = tr ? tr.parentNode : undefined;
  if (table) {
    table.removeChild(tr);
  }
  this.hideChilds();
};

/**
 * Recursively hide all childs
 */
JSONEditor.Node.prototype.hideChilds = function() {
  var childs = this.childs;
  if (!childs) {
    return;
  }
  if (!this.expanded) {
    return;
  }

  // hide append row
  var append = this.getAppend();
  if (append.parentNode) {
    append.parentNode.removeChild(append);
  }

  // hide childs
  this.childs.forEach(function(child) {
    child.hide();
  });
};

/**
 * Add a new child to the node.
 * Only applicable when Node value is of type array or object
 * @param {JSONEditor.Node} node
 */
JSONEditor.Node.prototype.appendChild = function(node) {
  if (this.type == "array" || this.type == "object") {
    // adjust the link to the parent
    node.setParent(this);
    node.fieldEditable = this.type == "object";
    if (this.type == "array") {
      node.index = this.childs.length;
    }
    this.childs.push(node);

    if (this.expanded) {
      // insert into the DOM, before the appendRow
      var newtr = node.getDom();
      var appendTr = this.getAppend();
      var table = appendTr ? appendTr.parentNode : undefined;
      if (appendTr && table) {
        table.insertBefore(newtr, appendTr);
      }

      node.showChilds();
    }

    this.updateDom({
      updateIndexes: true
    });
    node.updateDom({
      recurse: true
    });
  }
};

/**
 * Move a node from its current parent to this node
 * Only applicable when Node value is of type array or object
 * @param {JSONEditor.Node} node
 * @param {JSONEditor.Node} beforeNode
 */
JSONEditor.Node.prototype.moveBefore = function(node, beforeNode) {
  if (this.type == "array" || this.type == "object") {
    // create a temporary row, to prevent the scroll position from jumping
    // when removing the node
    var tbody = this.dom.tr ? this.dom.tr.parentNode : undefined;
    if (tbody) {
      var trTemp = document.createElement("tr");
      trTemp.style.height = tbody.clientHeight + "px";
      tbody.appendChild(trTemp);
    }

    var parent = node.getParent();
    if (parent) {
      parent.removeChild(node);
    }
    if (beforeNode instanceof JSONEditor.AppendNode) {
      this.appendChild(node);
    } else {
      this.insertBefore(node, beforeNode);
    }

    if (tbody) {
      tbody.removeChild(trTemp);
    }
  }
};

/**
 * Move a node from its current parent to this node
 * Only applicable when Node value is of type array or object.
 * If index is out of range, the node will be appended to the end
 * @param {JSONEditor.Node} node
 * @param {Number} index
 */
JSONEditor.Node.prototype.moveTo = function(node, index) {
  if (node.parent == this) {
    // same parent
    var currentIndex = this.childs.indexOf(node);
    if (currentIndex < index) {
      // compensate the index for removal of the node itself
      index++;
    }
  }

  var beforeNode = this.childs[index] || this.append;
  this.moveBefore(node, beforeNode);
};

/**
 * Insert a new child before a given node
 * Only applicable when Node value is of type array or object
 * @param {JSONEditor.Node} node
 * @param {JSONEditor.Node} beforeNode
 */
JSONEditor.Node.prototype.insertBefore = function(node, beforeNode) {
  if (this.type == "array" || this.type == "object") {
    if (beforeNode == this.append) {
      // append to the child nodes

      // adjust the link to the parent
      node.setParent(this);
      node.fieldEditable = this.type == "object";
      this.childs.push(node);
    } else {
      // insert before a child node
      var index = this.childs.indexOf(beforeNode);
      if (index == -1) {
        throw new Error("节点未找到.");
      }

      // adjust the link to the parent
      node.setParent(this);
      node.fieldEditable = this.type == "object";
      this.childs.splice(index, 0, node);
    }

    if (this.expanded) {
      // insert into the DOM
      var newTr = node.getDom();
      var nextTr = beforeNode.getDom();
      var table = nextTr ? nextTr.parentNode : undefined;
      if (nextTr && table) {
        table.insertBefore(newTr, nextTr);
      }

      node.showChilds();
    }

    this.updateDom({
      updateIndexes: true
    });
    node.updateDom({
      recurse: true
    });
  }
};

/**
 * Search in this node
 * The node will be expanded when the text is found one of its childs, else
 * it will be collapsed. Searches are case insensitive.
 * @param {String} text
 * @return {JSONEditor.Node[]} results  Array with nodes containing the search text
 */
JSONEditor.Node.prototype.search = function(text) {
  var results = [];
  var index;
  var search = text ? text.toLowerCase() : undefined;

  // delete old search data
  delete this.searchField;
  delete this.searchValue;

  // search in field
  if (this.field != undefined) {
    var field = String(this.field).toLowerCase();
    index = field.indexOf(search);
    if (index != -1) {
      this.searchField = true;
      results.push({
        node: this,
        elem: "field"
      });
    }

    // update dom
    this._updateDomField();
  }

  // search in value
  if (this.type == "array" || this.type == "object") {
    // array, object

    // search the nodes childs
    if (this.childs) {
      var childResults = [];
      this.childs.forEach(function(child) {
        childResults = childResults.concat(child.search(text));
      });
      results = results.concat(childResults);
    }

    // update dom
    if (search != undefined) {
      var recurse = false;
      if (childResults.length == 0) {
        this.collapse(recurse);
      } else {
        this.expand(recurse);
      }
    }
  } else {
    // string, auto
    if (this.value != undefined) {
      var value = String(this.value).toLowerCase();
      index = value.indexOf(search);
      if (index != -1) {
        this.searchValue = true;
        results.push({
          node: this,
          elem: "value"
        });
      }
    }

    // update dom
    this._updateDomValue();
  }

  return results;
};

/**
 * Move the scroll position such that this node is in the visible area.
 * The node will not get the focus
 */
JSONEditor.Node.prototype.scrollTo = function() {
  if (!this.dom.tr || !this.dom.tr.parentNode) {
    // if the node is not visible, expand its parents
    var parent = this.parent;
    var recurse = false;
    while (parent) {
      parent.expand(recurse);
      parent = parent.parent;
    }
  }

  if (this.dom.tr && this.dom.tr.parentNode) {
    this.editor.scrollTo(this.dom.tr.offsetTop);
  }
};

/**
 * Set focus to the value of this node
 * @param {String} [field]  The field name of the element to get the focus
 *                          available values: 'field', 'value'
 */
JSONEditor.Node.prototype.focus = function(field) {
  if (this.dom.tr && this.dom.tr.parentNode) {
    if (field != "value" && this.fieldEditable) {
      var domField = this.dom.field;
      if (domField) {
        domField.focus();
      }
    } else {
      var domValue = this.dom.value;
      if (domValue) {
        domValue.focus();
      }
    }
  }
};

/**
 * Update the values from the DOM field and value of this node
 */
JSONEditor.Node.prototype.blur = function() {
  // retrieve the actual field and value from the DOM.
  this._getDomValue(false);
  this._getDomField(false);
};

/**
 * Duplicate given child node
 * new structure will be added right before the cloned node
 * @param {JSONEditor.Node} node           the childNode to be duplicated
 * @return {JSONEditor.Node} clone         the clone of the node
 * @private
 */
JSONEditor.Node.prototype._duplicate = function(node) {
  var clone = node.clone();

  /* TODO: adjust the field name (to prevent equal field names)
     if (this.type == 'object') {
     }
     */

  // TODO: insert after instead of insert before
  this.insertBefore(clone, node);

  return clone;
};

/**
 * Check if given node is a child. The method will check recursively to find
 * this node.
 * @param {JSONEditor.Node} node
 * @return {boolean} containsNode
 */
JSONEditor.Node.prototype.containsNode = function(node) {
  if (this == node) {
    return true;
  }

  var childs = this.childs;
  if (childs) {
    // TOOD: use the js5 Array.some() here?
    for (var i = 0, iMax = childs.length; i < iMax; i++) {
      if (childs[i].containsNode(node)) {
        return true;
      }
    }
  }

  return false;
};

/**
 * Move given node into this node
 * @param {JSONEditor.Node} node           the childNode to be moved
 * @param {JSONEditor.Node} beforeNode     node will be inserted before given
 *                                         node. If no beforeNode is given,
 *                                         the node is appended at the end
 * @private
 */
JSONEditor.Node.prototype._move = function(node, beforeNode) {
  if (node == beforeNode) {
    // nothing to do...
    return;
  }

  // check if this node is not a child of the node to be moved here
  if (node.containsNode(this)) {
    throw new Error("不能把区域移动到自身的子节点.");
  }

  // remove the original node
  if (node.parent) {
    node.parent.removeChild(node);
  }

  // create a clone of the node
  var clone = node.clone();
  node.clearDom();

  // insert or append the node
  if (beforeNode) {
    this.insertBefore(clone, beforeNode);
  } else {
    this.appendChild(clone);
  }

  /* TODO: adjust the field name (to prevent equal field names)
     if (this.type == 'object') {
     }
     */
};

/**
 * Remove a child from the node.
 * Only applicable when Node value is of type array or object
 * @param {JSONEditor.Node} node   The child node to be removed;
 * @return {JSONEditor.Node | undefined} node  The removed node on success,
 *                                             else undefined
 */
JSONEditor.Node.prototype.removeChild = function(node) {
  if (this.childs) {
    var index = this.childs.indexOf(node);

    if (index != -1) {
      node.hide();

      // delete old search results
      delete node.searchField;
      delete node.searchValue;

      var removedNode = this.childs.splice(index, 1)[0];

      this.updateDom({
        updateIndexes: true
      });

      return removedNode;
    }
  }

  return undefined;
};

/**
 * Remove a child node node from this node
 * This method is equal to Node.removeChild, except that _remove firex an
 * onChange event.
 * @param {JSONEditor.Node} node
 * @private
 */
JSONEditor.Node.prototype._remove = function(node) {
  this.removeChild(node);
};

/**
 * Change the type of the value of this Node
 * @param {String} newType
 */
JSONEditor.Node.prototype.changeType = function(newType) {
  var oldType = this.type;

  if (
    (newType == "string" || newType == "auto") &&
    (oldType == "string" || oldType == "auto")
  ) {
    // this is an easy change
    this.type = newType;
  } else {
    // change from array to object, or from string/auto to object/array

    var table = this.dom.tr ? this.dom.tr.parentNode : undefined;
    var lastTr;
    if (this.expanded) {
      lastTr = this.getAppend();
    } else {
      lastTr = this.getDom();
    }
    var nextTr = lastTr && lastTr.parentNode ? lastTr.nextSibling : undefined;

    // hide current field and all its childs
    this.hide();
    this.clearDom();

    // adjust the field and the value
    this.type = newType;

    // adjust childs
    if (newType == "object") {
      if (!this.childs) {
        this.childs = [];
      }

      this.childs.forEach(function(child, index) {
        child.clearDom();
        delete child.index;
        child.fieldEditable = true;
        if (child.field == undefined) {
          child.field = index;
        }
      });

      if (oldType == "string" || oldType == "auto") {
        this.expanded = true;
      }
    } else if (newType == "array") {
      if (!this.childs) {
        this.childs = [];
      }

      this.childs.forEach(function(child, index) {
        child.clearDom();
        child.fieldEditable = false;
        child.index = index;
      });

      if (oldType == "string" || oldType == "auto") {
        this.expanded = true;
      }
    } else {
      this.expanded = false;
    }

    // create new DOM
    if (table) {
      if (nextTr) {
        table.insertBefore(this.getDom(), nextTr);
      } else {
        table.appendChild(this.getDom());
      }
    }
    this.showChilds();
  }

  if (newType == "auto" || newType == "string") {
    // cast value to the correct type
    if (newType == "string") {
      this.value = String(this.value);
    } else {
      this.value = this._stringCast(String(this.value));
    }

    this.focus();
  }

  this.updateDom({
    updateIndexes: true
  });
};

/**
 * Retrieve value from DOM
 * @param {boolean} silent.   If true (default), no errors will be thrown in
 *                            case of invalid data
 * @private
 */
JSONEditor.Node.prototype._getDomValue = function(silent) {
  if (this.dom.value && this.type != "array" && this.type != "object") {
    this.valueInnerText = JSONEditor.getInnerText(this.dom.value);
  }

  if (this.valueInnerText != undefined) {
    try {
      // retrieve the value
      var value;
      if (this.type == "string") {
        value = this._unescapeHTML(this.valueInnerText);
      } else {
        var str = this._unescapeHTML(this.valueInnerText);
        value = this._stringCast(str);
      }
      if (value !== this.value) {
        var oldValue = this.value;
        this.value = value;
        this.editor.onAction("editValue", {
          node: this,
          oldValue: oldValue,
          newValue: value
        });
      }
    } catch (err) {
      this.value = undefined;
      // TODO: sent an action with the new, invalid value?
      if (silent != true) {
        throw err;
      }
    }
  }
};

/**
 * Update dom value:
 * - the text color of the value, depending on the type of the value
 * - the height of the field, depending on the width
 * - background color in case it is empty
 * @private
 */
JSONEditor.Node.prototype._updateDomValue = function() {
  var domValue = this.dom.value;
  if (domValue) {
    // set text color depending on value type
    var v = this.value;
    var t = this.type == "auto" ? typeof v : this.type;
    var color = "";
    if (t == "string") {
      color = "green";
    } else if (t == "number") {
      color = "red";
    } else if (t == "boolean") {
      color = "blue";
    } else if (this.type == "object" || this.type == "array") {
      // note: typeof(null)=="object", therefore check this.type instead of t
      color = "";
    } else if (v === null) {
      color = "purple";
    } else if (v === undefined) {
      // invalid value
      color = "green";
    }
    domValue.style.color = color;

    // make backgound color lightgray when empty
    var isEmpty =
      String(this.value) == "" && this.type != "array" && this.type != "object";
    if (isEmpty) {
      JSONEditor.addClassName(domValue, "jsoneditor-empty");
    } else {
      JSONEditor.removeClassName(domValue, "jsoneditor-empty");
    }

    // highlight when there is a search result
    if (this.searchValueActive) {
      JSONEditor.addClassName(domValue, "jsoneditor-search-highlight-active");
    } else {
      JSONEditor.removeClassName(
        domValue,
        "jsoneditor-search-highlight-active"
      );
    }
    if (this.searchValue) {
      JSONEditor.addClassName(domValue, "jsoneditor-search-highlight");
    } else {
      JSONEditor.removeClassName(domValue, "jsoneditor-search-highlight");
    }

    // strip formatting from the contents of the editable div
    JSONEditor.stripFormatting(domValue);
  }
};

/**
 * Update dom field:
 * - the text color of the field, depending on the text
 * - the height of the field, depending on the width
 * - background color in case it is empty
 * @private
 */
JSONEditor.Node.prototype._updateDomField = function() {
  var domField = this.dom.field;
  if (domField) {
    // make backgound color lightgray when empty
    var isEmpty = String(this.field) == "";
    if (isEmpty) {
      JSONEditor.addClassName(domField, "jsoneditor-empty");
    } else {
      JSONEditor.removeClassName(domField, "jsoneditor-empty");
    }

    // highlight when there is a search result
    if (this.searchFieldActive) {
      JSONEditor.addClassName(domField, "jsoneditor-search-highlight-active");
    } else {
      JSONEditor.removeClassName(
        domField,
        "jsoneditor-search-highlight-active"
      );
    }
    if (this.searchField) {
      JSONEditor.addClassName(domField, "jsoneditor-search-highlight");
    } else {
      JSONEditor.removeClassName(domField, "jsoneditor-search-highlight");
    }

    // strip formatting from the contents of the editable div
    JSONEditor.stripFormatting(domField);
  }
};

/**
 * Retrieve field from DOM
 * @param {boolean} silent.   If true (default), no errors will be thrown in
 *                            case of invalid data
 * @private
 */
JSONEditor.Node.prototype._getDomField = function(silent) {
  if (this.dom.field && this.fieldEditable) {
    this.fieldInnerText = JSONEditor.getInnerText(this.dom.field);
  }

  if (this.fieldInnerText != undefined) {
    try {
      var field = this._unescapeHTML(this.fieldInnerText);

      if (field !== this.field) {
        var oldField = this.field;
        this.field = field;
        this.editor.onAction("editField", {
          node: this,
          oldValue: oldField,
          newValue: field
        });
      }
    } catch (err) {
      this.field = undefined;
      // TODO: sent an action here, with the new, invalid value?
      if (silent != true) {
        throw err;
      }
    }
  }
};

/**
 * Clear the dom of the node
 */
JSONEditor.Node.prototype.clearDom = function() {
  // TODO: hide the node first?
  //this.hide();
  // TOOD: recursively clear dom?

  this.dom = {};
};

/**
 * Get the HTML DOM TR element of the node.
 * The dom will be generated when not yet created
 * @return {Element} tr    HTML DOM TR Element
 */
JSONEditor.Node.prototype.getDom = function() {
  var dom = this.dom;
  if (dom.tr) {
    return dom.tr;
  }

  // create row
  dom.tr = document.createElement("tr");
  dom.tr.className = "jsoneditor-tr";
  dom.tr.node = this;

  if (this.editor.editable) {
    // create draggable area
    var tdDrag = document.createElement("td");
    tdDrag.className = "jsoneditor-td";
    dom.drag = this._createDomDragArea();
    if (dom.drag) {
      tdDrag.appendChild(dom.drag);
    }
    dom.tr.appendChild(tdDrag);
  }

  // create tree and field
  var tdField = document.createElement("td");
  tdField.className = "jsoneditor-td";
  dom.tr.appendChild(tdField);
  dom.expand = this._createDomExpandButton();
  dom.field = this._createDomField();
  dom.value = this._createDomValue();
  dom.tree = this._createDomTree(dom.expand, dom.field, dom.value);
  tdField.appendChild(dom.tree);

  if (this.editor.editable) {
    // create type select box
    var tdType = document.createElement("td");
    tdType.className = "jsoneditor-td jsoneditor-td-edit";
    dom.tr.appendChild(tdType);
    dom.type = this._createDomTypeButton();
    tdType.appendChild(dom.type);

    // create duplicate button
    var tdDuplicate = document.createElement("td");
    tdDuplicate.className = "jsoneditor-td jsoneditor-td-edit";
    dom.tr.appendChild(tdDuplicate);
    dom.duplicate = this._createDomDuplicateButton();
    if (dom.duplicate) {
      tdDuplicate.appendChild(dom.duplicate);
    }

    // create remove button
    var tdRemove = document.createElement("td");
    tdRemove.className = "jsoneditor-td jsoneditor-td-edit";
    dom.tr.appendChild(tdRemove);
    dom.remove = this._createDomRemoveButton();
    if (dom.remove) {
      tdRemove.appendChild(dom.remove);
    }
  }

  this.updateDom(); // TODO: recurse here?

  return dom.tr;
};

/**
 * DragStart event, fired on mousedown on the dragarea at the left side of a Node
 * @param {Event} event
 * @private
 */
JSONEditor.Node.prototype._onDragStart = function(event) {
  event = event || window.event;

  var node = this;
  if (!this.mousemove) {
    this.mousemove = JSONEditor.Events.addEventListener(
      document,
      "mousemove",

      function(event) {
        node._onDrag(event);
      }
    );
  }

  if (!this.mouseup) {
    this.mouseup = JSONEditor.Events.addEventListener(
      document,
      "mouseup",

      function(event) {
        node._onDragEnd(event);
      }
    );
  }

  /* TODO: correct highlighting when the TypeDropDown is visible (And has highlighting locked)
     if (JSONEditor.freezeHighlight) {
     console.log('heee');
     JSONEditor.freezeHighlight = false;
     this.setHighlight(true);
     }
     */
  JSONEditor.freezeHighlight = true;
  this.drag = {
    oldCursor: document.body.style.cursor,
    startParent: this.parent,
    startIndex: this.parent.childs.indexOf(this)
  };
  document.body.style.cursor = "move";

  JSONEditor.Events.preventDefault(event);
};

/**
 * Drag event, fired when moving the mouse while dragging a Node
 * @param {Event} event
 * @private
 */
JSONEditor.Node.prototype._onDrag = function(event) {
  event = event || window.event;
  var trThis = this.dom.tr;

  // TODO: add an ESC option, which resets to the original position

  var topThis = JSONEditor.getAbsoluteTop(trThis);
  var heightThis = trThis.offsetHeight;
  var mouseY = event.pageY || event.clientY + document.body.scrollTop;
  if (mouseY < topThis) {
    // move up
    var trPrev = trThis.previousSibling;
    var topPrev = JSONEditor.getAbsoluteTop(trPrev);
    var nodePrev = JSONEditor.getNodeFromTarget(trPrev);
    while (trPrev && mouseY < topPrev) {
      nodePrev = JSONEditor.getNodeFromTarget(trPrev);
      trPrev = trPrev.previousSibling;
      topPrev = JSONEditor.getAbsoluteTop(trPrev);
    }

    if (nodePrev) {
      trPrev = nodePrev.dom.tr;
      topPrev = JSONEditor.getAbsoluteTop(trPrev);
      if (mouseY > topPrev + heightThis) {
        nodePrev = undefined;
      }
    }

    if (nodePrev && nodePrev.parent) {
      nodePrev.parent.moveBefore(this, nodePrev);
    }
  } else {
    // move down
    var trLast =
      this.expanded && this.append ? this.append.getDom() : this.dom.tr;
    var trFirst = trLast ? trLast.nextSibling : undefined;
    if (trFirst) {
      var topFirst = JSONEditor.getAbsoluteTop(trFirst);

      var nodeNext = undefined;
      var trNext = trFirst.nextSibling;
      var topNext = JSONEditor.getAbsoluteTop(trNext);
      var heightNext = trNext ? topNext - topFirst : 0;
      while (trNext && mouseY > topThis + heightNext) {
        nodeNext = JSONEditor.getNodeFromTarget(trNext);
        trNext = trNext.nextSibling;
        topNext = JSONEditor.getAbsoluteTop(trNext);
        heightNext = trNext ? topNext - topFirst : 0;
      }

      if (nodeNext && nodeNext.parent) {
        nodeNext.parent.moveBefore(this, nodeNext);
      }
    }
  }
  JSONEditor.Events.preventDefault(event);
};

/**
 * Drag event, fired on mouseup after having dragged a node
 * @param {Event} event
 * @private
 */
JSONEditor.Node.prototype._onDragEnd = function(event) {
  event = event || window.event;

  var params = {
    node: this,
    startParent: this.drag.startParent,
    startIndex: this.drag.startIndex,
    endParent: this.parent,
    endIndex: this.parent.childs.indexOf(this)
  };
  if (
    params.startParent != params.endParent ||
    params.startIndex != params.endIndex
  ) {
    // only register this action if the node is actually moved to another place
    this.editor.onAction("moveNode", params);
  }

  document.body.style.cursor = this.drag.oldCursor;
  delete JSONEditor.freezeHighlight;
  delete this.drag;
  this.setHighlight(false);

  if (this.mousemove) {
    JSONEditor.Events.removeEventListener(
      document,
      "mousemove",
      this.mousemove
    );
    delete this.mousemove;
  }
  if (this.mouseup) {
    JSONEditor.Events.removeEventListener(document, "mouseup", this.mouseup);
    delete this.mouseup;
  }

  JSONEditor.Events.preventDefault(event);
};

/**
 * Create a drag area, displayed at the left side of the node
 * @return {Element | undefined} domDrag
 * @private
 */
JSONEditor.Node.prototype._createDomDragArea = function() {
  if (!this.parent) {
    return undefined;
  }

  var domDrag = document.createElement("button");
  domDrag.className = "jsoneditor-dragarea";
  domDrag.title = "Move field (drag and drop)";

  return domDrag;
};

/**
 * Create an editable field
 * @return {Element} domField
 * @private
 */
JSONEditor.Node.prototype._createDomField = function() {
  return document.createElement("div");
};

/**
 * Set highlighting for this node and all its childs.
 * Only applied to the currently visible (expanded childs)
 * @param {boolean} highlight
 */
JSONEditor.Node.prototype.setHighlight = function(highlight) {
  if (JSONEditor.freezeHighlight) {
    return;
  }

  if (this.dom.tr) {
    this.dom.tr.className =
      "jsoneditor-tr" + (highlight ? " jsoneditor-tr-highlight" : "");

    if (this.append) {
      this.append.setHighlight(highlight);
    }

    if (this.childs) {
      this.childs.forEach(function(child) {
        child.setHighlight(highlight);
      });
    }
  }
};

/**
 * Update the value of the node. Only primitive types are allowed, no Object
 * or Array is allowed.
 * @param {String | Number | Boolean | null} value
 */
JSONEditor.Node.prototype.updateValue = function(value) {
  this.value = value;
  this.updateDom();
};

/**
 * Update the field of the node.
 * @param {String} field
 */
JSONEditor.Node.prototype.updateField = function(field) {
  this.field = field;
  this.updateDom();
};

/**
 * Update the HTML DOM, optionally recursing through the childs
 * @param {Object} [options] Available parameters:
 *                          {boolean} [recurse]         If true, the
 *                          DOM of the childs will be updated recursively.
 *                          False by default.
 *                          {boolean} [updateIndexes]   If true, the childs
 *                          indexes of the node will be updated too. False by
 *                          default.
 */
JSONEditor.Node.prototype.updateDom = function(options) {
  // update level indentation
  var domTree = this.dom.tree;
  if (domTree) {
    domTree.style.marginLeft = this.getLevel() * 24 + "px";
  }

  // update field
  var domField = this.dom.field;
  if (domField) {
    if (this.fieldEditable == true) {
      // parent is an object
      domField.contentEditable = this.editor.editable;
      domField.spellcheck = false;
      domField.className = "jsoneditor-field";
    } else {
      // parent is an array this is the root node
      domField.className = "jsoneditor-readonly";
    }

    var field;
    if (this.index != undefined) {
      field = this.index;
    } else if (this.field != undefined) {
      field = this.field;
    } else if (this.type == "array" || this.type == "object") {
      field = this.type;
    } else {
      field = "field";
    }
    domField.innerHTML = this._escapeHTML(field);
  }

  // update value
  var domValue = this.dom.value;
  if (domValue) {
    var count = this.childs ? this.childs.length : 0;
    if (this.type == "array") {
      domValue.innerHTML = "[" + count + "]";
      domValue.title = this.type + " containing " + count + " items";
    } else if (this.type == "object") {
      domValue.innerHTML = "{" + count + "}";
      domValue.title = this.type + " containing " + count + " items";
    } else {
      domValue.innerHTML = this._escapeHTML(this.value);
      delete domValue.title;
    }
  }

  // update field and value
  this._updateDomField();
  this._updateDomValue();

  // update childs indexes
  if (options && options.updateIndexes == true) {
    // updateIndexes is true or undefined
    this._updateDomIndexes();
  }

  if (options && options.recurse == true) {
    // recurse is true or undefined. update childs recursively
    if (this.childs) {
      this.childs.forEach(function(child) {
        child.updateDom(options);
      });
    }

    // update row with append button
    if (this.append) {
      this.append.updateDom();
    }
  }
};

/**
 * Update the DOM of the childs of a node: update indexes and undefined field
 * names.
 * Only applicable when structure is an array or object
 * @private
 */
JSONEditor.Node.prototype._updateDomIndexes = function() {
  var domValue = this.dom.value;
  var childs = this.childs;
  if (domValue && childs) {
    if (this.type == "array") {
      childs.forEach(function(child, index) {
        child.index = index;
        var childField = child.dom.field;
        if (childField) {
          childField.innerHTML = index;
        }
      });
    } else if (this.type == "object") {
      childs.forEach(function(child) {
        if (child.index != undefined) {
          delete child.index;

          if (child.field == undefined) {
            child.field = "field";
          }
        }
      });
    }
  }
};

/**
 * Create an editable value
 * @private
 */
JSONEditor.Node.prototype._createDomValue = function() {
  var domValue;

  if (this.type == "array") {
    domValue = document.createElement("div");
    domValue.className = "jsoneditor-readonly";
    domValue.innerHTML = "[...]";
  } else if (this.type == "object") {
    domValue = document.createElement("div");
    domValue.className = "jsoneditor-readonly";
    domValue.innerHTML = "{...}";
  } else if (this.type == "string") {
    domValue = document.createElement("div");
    domValue.contentEditable = this.editor.editable;
    domValue.spellcheck = false;
    domValue.className = "jsoneditor-value";
    domValue.innerHTML = this._escapeHTML(this.value);
  } else {
    domValue = document.createElement("div");
    domValue.contentEditable = this.editor.editable;
    domValue.spellcheck = false;
    domValue.className = "jsoneditor-value";
    domValue.innerHTML = this._escapeHTML(this.value);
  }

  // TODO: in FF spel/check of editable divs is done via the body. quite ugly
  // document.body.spellcheck = false;

  return domValue;
};

/**
 * Create an expand/collapse button
 * @return {Element} expand
 * @private
 */
JSONEditor.Node.prototype._createDomExpandButton = function() {
  // create expand button
  var expand = document.createElement("button");
  var expandable = this.type == "array" || this.type == "object";
  if (expandable) {
    expand.className = this.expanded
      ? "jsoneditor-expanded"
      : "jsoneditor-collapsed";
    expand.title =
      "Click to expand/collapse this field. \n" +
      "Ctrl+Click to expand/collapse including all childs.";
  } else {
    expand.className = "jsoneditor-invisible";
    expand.title = "";
  }

  return expand;
};

/**
 * Create a DOM tree element, containing the expand/collapse button
 * @param {Element} domExpand
 * @param {Element} domField
 * @param {Element} domValue
 * @return {Element} domTree
 * @private
 */
JSONEditor.Node.prototype._createDomTree = function(
  domExpand,
  domField,
  domValue
) {
  var dom = this.dom;
  var domTree = document.createElement("table");
  var tbody = document.createElement("tbody");
  domTree.style.borderCollapse = "collapse"; // TODO: put in css
  domTree.appendChild(tbody);
  var tr = document.createElement("tr");
  tbody.appendChild(tr);

  // create expand button
  var tdExpand = document.createElement("td");
  tdExpand.className = "jsoneditor-td-tree";
  tr.appendChild(tdExpand);
  tdExpand.appendChild(domExpand);
  dom.tdExpand = tdExpand;

  // add the field
  var tdField = document.createElement("td");
  tdField.className = "jsoneditor-td-tree";
  tr.appendChild(tdField);
  tdField.appendChild(domField);
  dom.tdField = tdField;

  // add a separator
  var tdSeparator = document.createElement("td");
  tdSeparator.className = "jsoneditor-td-tree";
  tr.appendChild(tdSeparator);
  if (this.type != "object" && this.type != "array") {
    tdSeparator.appendChild(document.createTextNode(":"));
    tdSeparator.className = "jsoneditor-separator";
  }
  dom.tdSeparator = tdSeparator;

  // add the value
  var tdValue = document.createElement("td");
  tdValue.className = "jsoneditor-td-tree";
  tr.appendChild(tdValue);
  tdValue.appendChild(domValue);
  dom.tdValue = tdValue;

  return domTree;
};

/**
 * Handle an event. The event is catched centrally by the editor
 * @param {Event} event
 */
JSONEditor.Node.prototype.onEvent = function(event) {
  var type = event.type;
  var target = event.target || event.srcElement;
  var dom = this.dom;
  var node = this;
  var expandable = this.type == "array" || this.type == "object";

  // value events
  var domValue = dom.value;
  if (target == domValue) {
    switch (type) {
      case "focus":
        JSONEditor.focusNode = this;
        break;

      case "blur":
      case "change":
        this._getDomValue(true);
        this._updateDomValue();
        if (this.value) {
          domValue.innerHTML = this._escapeHTML(this.value);
        }
        break;

      case "keyup":
        this._getDomValue(true);
        this._updateDomValue();
        break;

      case "cut":
      case "paste":
        setTimeout(function() {
          node._getDomValue(true);
          node._updateDomValue();
        }, 1);
        break;
    }
  }

  // field events
  var domField = dom.field;
  if (target == domField) {
    switch (type) {
      case "focus":
        JSONEditor.focusNode = this;
        break;

      case "change":
      case "blur":
        this._getDomField(true);
        this._updateDomField();
        if (this.field) {
          domField.innerHTML = this._escapeHTML(this.field);
        }
        break;

      case "keyup":
        this._getDomField(true);
        this._updateDomField();
        break;

      case "cut":
      case "paste":
        setTimeout(function() {
          node._getDomField(true);
          node._updateDomField();
        }, 1);
        break;
    }
  }

  // drag events
  var domDrag = dom.drag;
  if (target == domDrag) {
    switch (type) {
      case "mousedown":
        this._onDragStart(event);
        break;
      case "mouseover":
        this.setHighlight(true);
        break;
      case "mouseout":
        this.setHighlight(false);
        break;
    }
  }

  // expand events
  var domExpand = dom.expand;
  if (target == domExpand) {
    if (type == "click") {
      if (expandable) {
        this._onExpand(event);
      }
    }
  }

  // duplicate button
  var domDuplicate = dom.duplicate;
  if (target == domDuplicate) {
    switch (type) {
      case "click":
        var clone = this.parent._duplicate(this);

        this.editor.onAction("duplicateNode", {
          node: this,
          clone: clone,
          parent: this.parent
        });
        break;
      case "mouseover":
        this.setHighlight(true);
        break;
      case "mouseout":
        this.setHighlight(false);
        break;
    }
  }

  // remove button
  var domRemove = dom.remove;
  if (target == domRemove) {
    switch (type) {
      case "click":
        this._onRemove();
        break;
      case "mouseover":
        this.setHighlight(true);
        break;
      case "mouseout":
        this.setHighlight(false);
        break;
    }
  }

  // type button
  var domType = dom.type;
  if (target == domType) {
    switch (type) {
      case "click":
        this._onChangeType(event);
        break;
      case "mouseover":
        this.setHighlight(true);
        break;
      case "mouseout":
        this.setHighlight(false);
        break;
    }
  }

  // focus
  // when clicked in whitespace left or right from the field or value, set focus
  var domTree = dom.tree;
  if (target == domTree.parentNode) {
    switch (type) {
      case "click":
        var left =
          event.offsetX != undefined
            ? event.offsetX < (this.getLevel() + 1) * 24
            : event.clientX < JSONEditor.getAbsoluteLeft(dom.tdSeparator); // for FF
        if (left || expandable) {
          // node is expandable when it is an object or array
          if (domField) {
            JSONEditor.setEndOfContentEditable(domField);
            domField.focus();
          }
        } else {
          if (domValue) {
            JSONEditor.setEndOfContentEditable(domValue);
            domValue.focus();
          }
        }
        break;
    }
  }

  if (
    (target == dom.tdExpand && !expandable) ||
    target == dom.tdField ||
    target == dom.tdSeparator
  ) {
    switch (type) {
      case "click":
        if (domField) {
          JSONEditor.setEndOfContentEditable(domField);
          domField.focus();
        }
        break;
    }
  }
};

/**
 * Handle the expand event, when clicked on the expand button
 * @param {Event} event
 * @private
 */
JSONEditor.Node.prototype._onExpand = function(event) {
  event = event || window.event;
  var recurse = event.ctrlKey; // with ctrl-key, expand/collapse all

  if (recurse) {
    // Take the table offline
    var table = this.dom.tr.parentNode; // TODO: not nice to access the main table like this
    var frame = table.parentNode;
    var scrollTop = frame.scrollTop;
    frame.removeChild(table);
  }

  if (this.expanded) {
    this.collapse(recurse);
  } else {
    this.expand(recurse);
  }

  if (recurse) {
    // Put the table online again
    frame.appendChild(table);
    frame.scrollTop = scrollTop;
  }
};

JSONEditor.Node.types = [
  {
    value: "array",
    className: "jsoneditor-option-array",
    title: '"array" 类型: 一个包含了有序值集合的数组.'
  },
  {
    value: "auto",
    className: "jsoneditor-option-auto",
    title:
      '"auto" 类型: 节点类型将自动从值中获取, 可以是: string, number, boolean, 或者 null.'
  },
  {
    value: "object",
    className: "jsoneditor-option-object",
    title: '"object" 类型: 对象包含了一些无序的键/值对.'
  },
  {
    value: "string",
    className: "jsoneditor-option-string",
    title: '"string" 类型: 节点类型不从值中自动获取, 但永远返回 string.'
  }
];

/**
 * Create a DOM select box containing the node type
 * @return {Element} domType
 * @private
 */
JSONEditor.Node.prototype._createDomTypeButton = function() {
  var node = this;
  var domType = document.createElement("button");
  domType.className = "jsoneditor-type-" + node.type;
  domType.title = "改变节点类型";

  return domType;
};

/**
 * Remove this node
 * @private
 */
JSONEditor.Node.prototype._onRemove = function() {
  this.setHighlight(false);
  var index = this.parent.childs.indexOf(this);

  this.parent._remove(this);

  this.editor.onAction("removeNode", {
    node: this,
    parent: this.parent,
    index: index
  });
};

/**
 * Handle a click on the Type-button
 * @param {Event} event
 * @private
 */
JSONEditor.Node.prototype._onChangeType = function(event) {
  JSONEditor.Events.stopPropagation(event);

  var domType = this.dom.type;

  var node = this;
  var x = JSONEditor.getAbsoluteLeft(domType);
  var y = JSONEditor.getAbsoluteTop(domType) + domType.clientHeight;
  var callback = function(newType) {
    var oldType = node.type;
    node.changeType(newType);
    node.editor.onAction("changeType", {
      node: node,
      oldType: oldType,
      newType: newType
    });
    domType.className = "jsoneditor-type-" + node.type;
  };
  JSONEditor.showDropDownList({
    x: x,
    y: y,
    node: node,
    value: node.type,
    values: JSONEditor.Node.types,
    className: "jsoneditor-select",
    optionSelectedClassName: "jsoneditor-option-selected",
    optionClassName: "jsoneditor-option",
    callback: callback
  });
};

/**
 * Show a dropdown list
 * @param {Object} params    Available parameters:
 *                           {Number} x  The absolute horizontal position
 *                           {Number} y  The absolute vertical position
 *                           {JSONEditor.Node} node node used for highlighting
 *                           {String} value current selected value
 *                           {Object[]} values the available values. Each object
 *                                             contains a value, title, and
 *                                             className
 *                           {String} optionSelectedClassName
 *                           {String} optionClassName
 *                           {function} callback   Callback method, called when
 *                                                 the selected value changed.
 */
JSONEditor.showDropDownList = function(params) {
  var select = document.createElement("div");
  select.className = params.className || "";
  select.style.position = "absolute";
  select.style.left = (params.x || 0) + "px";
  select.style.top = (params.y || 0) + "px";

  params.values.forEach(function(v) {
    var text = v.value || String(v);
    var className = "jsoneditor-option";
    var selected = text == params.value;
    if (selected) {
      className += " " + params.optionSelectedClassName;
    }
    var option = document.createElement("div");
    option.className = className;
    if (v.title) {
      option.title = v.title;
    }

    var divIcon = document.createElement("div");
    divIcon.className = v.className || "";
    option.appendChild(divIcon);

    var divText = document.createElement("div");
    divText.className = "jsoneditor-option-text";
    divText.innerHTML = "<div>" + text + "</div>";
    option.appendChild(divText);

    option.onmousedown = (function(value) {
      return function() {
        params.callback(value);
      };
    })(v.value);
    select.appendChild(option);
  });

  document.body.appendChild(select);
  params.node.setHighlight(true);
  JSONEditor.freezeHighlight = true;

  // TODO: change to onclick? -> but be sure to remove existing dropdown first
  var onmousedown = JSONEditor.Events.addEventListener(
    document,
    "mousedown",
    function() {
      JSONEditor.freezeHighlight = false;
      params.node.setHighlight(false);
      if (select && select.parentNode) {
        select.parentNode.removeChild(select);
      }
      JSONEditor.Events.removeEventListener(document, "mousedown", onmousedown);
    }
  );
  var onmousewheel = JSONEditor.Events.addEventListener(
    document,
    "mousewheel",
    function() {
      JSONEditor.freezeHighlight = false;
      params.node.setHighlight(false);
      if (select && select.parentNode) {
        select.parentNode.removeChild(select);
      }
      JSONEditor.Events.removeEventListener(
        document,
        "mousewheel",
        onmousewheel
      );
    }
  );
};

/**
 * Create a table row with an append button.
 * @return {Element | undefined} buttonAppend or undefined when inapplicable
 */
JSONEditor.Node.prototype.getAppend = function() {
  if (!this.append) {
    this.append = new JSONEditor.AppendNode(this.editor);
    this.append.setParent(this);
  }
  return this.append.getDom();
};

/**
 * Create a remove button. Returns undefined when the structure cannot
 * be removed
 * @return {Element | undefined} removeButton, or undefined when inapplicable
 * @private
 */
JSONEditor.Node.prototype._createDomRemoveButton = function() {
  if (
    this.parent &&
    (this.parent.type == "array" || this.parent.type == "object")
  ) {
    var buttonRemove = document.createElement("button");
    buttonRemove.className = "jsoneditor-remove";
    buttonRemove.title = "删除节点 (包括所有子节点)";

    return buttonRemove;
  } else {
    return undefined;
  }
};

/**
 * Create a duplicate button.
 * If the Node is the root node, no duplicate button is available and undefined
 * will be returned
 * @return {Element | undefined} buttonDuplicate
 * @private
 */
JSONEditor.Node.prototype._createDomDuplicateButton = function() {
  if (
    this.parent &&
    (this.parent.type == "array" || this.parent.type == "object")
  ) {
    var buttonDupliate = document.createElement("button");
    buttonDupliate.className = "jsoneditor-duplicate";
    buttonDupliate.title = "复制节点 (包括所有子节点)";

    return buttonDupliate;
  } else {
    return undefined;
  }
};

/**
 * get the type of a value
 * @param {*} value
 * @return {String} type   Can be 'object', 'array', 'string', 'auto'
 * @private
 */
JSONEditor.Node.prototype._getType = function(value) {
  if (value instanceof Array) {
    return "array";
  }
  if (value instanceof Object) {
    return "object";
  }
  if (typeof value == "string" && typeof this._stringCast(value) != "string") {
    return "string";
  }

  return "auto";
};

/**
 * cast contents of a string to the correct type. This can be a string,
 * a number, a boolean, etc
 * @param {String} str
 * @return {*} castedStr
 * @private
 */
JSONEditor.Node.prototype._stringCast = function(str) {
  var lower = str.toLowerCase(),
    num = Number(str), // will nicely fail with '123ab'
    numFloat = parseFloat(str); // will nicely fail with '  '

  if (str == "") {
    return "";
  } else if (lower == "null") {
    return null;
  } else if (lower == "true") {
    return true;
  } else if (lower == "false") {
    return false;
  } else if (!isNaN(num) && !isNaN(numFloat)) {
    return num;
  } else {
    return str;
  }
};

/**
 * escape a text, such that it can be displayed safely in an HTML element
 * @param {String} text
 * @return {String} escapedText
 * @private
 */
JSONEditor.Node.prototype._escapeHTML = function(text) {
  var htmlEscaped = String(text)
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/  /g, " &nbsp;") // replace double space with an nbsp and space
    .replace(/^ /, "&nbsp;") // space at start
    .replace(/ $/, "&nbsp;"); // space at end

  var json = JSON.stringify(htmlEscaped);
  return json.substring(1, json.length - 1);
};

/**
 * unescape a string.
 * @param {String} escapedText
 * @return {String} text
 * @private
 */
JSONEditor.Node.prototype._unescapeHTML = function(escapedText) {
  var json = '"' + this._escapeJSON(escapedText) + '"';
  var htmlEscaped = JSONEditor.parse(json);
  return htmlEscaped
    .replace(/&lt;/g, "<")
    .replace(/&gt;/g, ">")
    .replace(/&nbsp;/g, " ");
};

/**
 * escape a text to make it a valid JSON string. The method will:
 *   - replace unescaped double quotes with '\"'
 *   - replace unescaped backslash with '\\'
 *   - replace returns with '\n'
 * @param {String} text
 * @return {String} escapedText
 * @private
 */
JSONEditor.Node.prototype._escapeJSON = function(text) {
  // TODO: replace with some smart regex (only when a new solution is faster!)
  var escaped = "";
  var i = 0,
    iMax = text.length;
  while (i < iMax) {
    var c = text.charAt(i);
    if (c == "\n") {
      escaped += "\\n";
    } else if (c == "\\") {
      escaped += c;
      i++;

      c = text.charAt(i);
      if ('"\\/bfnrtu'.indexOf(c) == -1) {
        escaped += "\\"; // no valid escape character
      }
      escaped += c;
    } else if (c == '"') {
      escaped += '\\"';
    } else {
      escaped += c;
    }
    i++;
  }

  return escaped;
};

/**
 * @constructor JSONEditor.AppendNode
 * @extends JSONEditor.Node
 * @param {JSONEditor} editor
 * Create a new AppendNode. This is a special node which is created at the
 * end of the list with childs for an object or array
 */
JSONEditor.AppendNode = function(editor) {
  this.editor = editor;
  this.dom = {};
};

JSONEditor.AppendNode.prototype = new JSONEditor.Node();

/**
 * Return a table row with an append button.
 * @return {Element} dom   TR element
 */
JSONEditor.AppendNode.prototype.getDom = function() {
  if (this.dom.tr) {
    return this.dom.tr;
  }

  /**
   * Create a TD element, and give it the provided class name (if any)
   * @param {String} [className]
   * @return {Element} td
   */
  function newTd(className) {
    var td = document.createElement("td");
    td.className = className || "";
    return td;
  }

  // a row for the append button
  var trAppend = document.createElement("tr");
  trAppend.node = this;

  // TODO: do not create an appendNode at all when in viewer mode
  if (!this.editor.editable) {
    return trAppend;
  }

  // a cell for the drag area column
  trAppend.appendChild(newTd("jsoneditor-td"));

  // a cell for the append button
  var tdAppend = document.createElement("td");
  trAppend.appendChild(tdAppend);
  tdAppend.className = "jsoneditor-td";

  // create the append button
  var buttonAppend = document.createElement("button");
  buttonAppend.className = "jsoneditor-append";
  buttonAppend.title = "添加";
  this.dom.append = buttonAppend;
  tdAppend.appendChild(buttonAppend);

  trAppend.appendChild(newTd("jsoneditor-td jsoneditor-td-edit"));
  trAppend.appendChild(newTd("jsoneditor-td jsoneditor-td-edit"));
  trAppend.appendChild(newTd("jsoneditor-td jsoneditor-td-edit"));

  this.dom.tr = trAppend;
  this.dom.td = tdAppend;

  this.updateDom();

  return trAppend;
};

/**
 * Update the HTML dom of the Node
 */
JSONEditor.AppendNode.prototype.updateDom = function() {
  var tdAppend = this.dom.td;
  if (tdAppend) {
    tdAppend.style.paddingLeft = this.getLevel() * 24 + 26 + "px";
    // TODO: not so nice hard coded offset
  }
};

/**
 * Handle an event. The event is catched centrally by the editor
 * @param {Event} event
 */
JSONEditor.AppendNode.prototype.onEvent = function(event) {
  var type = event.type;
  var target = event.target || event.srcElement;
  var dom = this.dom;

  var domAppend = dom.append;
  if (target == domAppend) {
    switch (type) {
      case "click":
        this._onAppend();
        break;

      case "mouseover":
        this.parent.setHighlight(true);
        break;

      case "mouseout":
        this.parent.setHighlight(false);
    }
  }
};

/**
 * Handle append event
 * @private
 */
JSONEditor.AppendNode.prototype._onAppend = function() {
  var newNode = new JSONEditor.Node(this.editor, {
    field: "field",
    value: "value"
  });
  this.parent.appendChild(newNode);
  this.parent.setHighlight(false);
  newNode.focus();

  this.editor.onAction("appendNode", {
    node: newNode,
    parent: this.parent
  });
};

/**
 * Create main frame
 * @private
 */
JSONEditor.prototype._createFrame = function() {
  // create the frame
  this.container.innerHTML = "";
  this.frame = document.createElement("div");
  this.frame.className = "jsoneditor-frame";
  this.container.appendChild(this.frame);

  // create one global event listener to handle all events from all nodes
  var editor = this;
  // TODO: move this onEvent to JSONEditor.prototype.onEvent
  var onEvent = function(event) {
    event = event || window.event;
    var target = event.target || event.srcElement;

    /* TODO: Enable quickkeys Ctrl+F and F3.
        //       Requires knowing whether the JSONEditor has focus or not
        //       (use a global event listener for that?)
        // Check for search quickkeys, Ctrl+F and F3
        if (editor.options.search) {
            if (event.type == 'keydown') {
                var keynum = event.which || event.keyCode;
                if (keynum == 70 && event.ctrlKey) { // Ctrl+F
                    if (editor.searchBox) {
                        editor.searchBox.dom.search.focus();
                        editor.searchBox.dom.search.select();
                        JSONEditor.Events.preventDefault(event);
                        JSONEditor.Events.stopPropagation(event);
                    }
                }
                else if (keynum == 114) { // F3
                    if (!event.shiftKey) {
                        // select next search result
                        editor.searchBox.next();
                    }
                    else {
                        // select previous search result
                        editor.searchBox.previous();
                    }
                    editor.searchBox.focusActiveResult();

                    // set selection to the current
                    JSONEditor.Events.preventDefault(event);
                    JSONEditor.Events.stopPropagation(event);
                }
            }
        }
        */

    var node = JSONEditor.getNodeFromTarget(target);
    if (node) {
      node.onEvent(event);
    }
  };
  this.frame.onclick = function(event) {
    onEvent(event);

    // prevent default submit action when JSONEditor is located inside a form
    JSONEditor.Events.preventDefault(event);
  };
  this.frame.onchange = onEvent;
  this.frame.onkeydown = onEvent;
  this.frame.onkeyup = onEvent;
  this.frame.oncut = onEvent;
  this.frame.onpaste = onEvent;
  this.frame.onmousedown = onEvent;
  this.frame.onmouseup = onEvent;
  this.frame.onmouseover = onEvent;
  this.frame.onmouseout = onEvent;
  // Note: focus and blur events do not propagate, therefore they defined
  // using an eventListener with useCapture=true
  // see http://www.quirksmode.org/blog/archives/2008/04/delegating_the.html
  JSONEditor.Events.addEventListener(this.frame, "focus", onEvent, true);
  JSONEditor.Events.addEventListener(this.frame, "blur", onEvent, true);
  this.frame.onfocusin = onEvent; // for IE
  this.frame.onfocusout = onEvent; // for IE

  // create menu
  this.menu = document.createElement("div");
  this.menu.className = "jsoneditor-menu";
  this.frame.appendChild(this.menu);

  // create expand all button
  var expandAll = document.createElement("button");
  expandAll.className = "jsoneditor-menu jsoneditor-expand-all";
  expandAll.title = "展开";
  expandAll.onclick = function() {
    editor.expandAll();
  };
  this.menu.appendChild(expandAll);

  // create expand all button
  var collapseAll = document.createElement("button");
  collapseAll.title = "折叠";
  collapseAll.className = "jsoneditor-menu jsoneditor-collapse-all";
  collapseAll.onclick = function() {
    editor.collapseAll();
  };
  this.menu.appendChild(collapseAll);

  // create expand/collapse buttons
  if (this.history) {
    // create separator
    var separator = document.createElement("span");
    separator.innerHTML = "&nbsp;";
    this.menu.appendChild(separator);

    // create undo button
    var undo = document.createElement("button");
    undo.className = "jsoneditor-menu jsoneditor-undo";
    undo.title = "撤销";
    undo.onclick = function() {
      // undo last action
      editor.history.undo();

      // trigger change callback
      if (editor.options.change) {
        editor.options.change();
      }
    };
    this.menu.appendChild(undo);
    this.dom.undo = undo;

    // create redo button
    var redo = document.createElement("button");
    redo.className = "jsoneditor-menu jsoneditor-redo";
    redo.title = "重做";
    redo.onclick = function() {
      // redo last action
      editor.history.redo();

      // trigger change callback
      if (editor.options.change) {
        editor.options.change();
      }
    };
    this.menu.appendChild(redo);
    this.dom.redo = redo;

    // register handler for onchange of history
    this.history.onChange = function() {
      undo.disabled = !editor.history.canUndo();
      redo.disabled = !editor.history.canRedo();
    };
    this.history.onChange();
  }

  // create search box
  if (this.options.search) {
    this.searchBox = new JSONEditor.SearchBox(this, this.menu);
  }
};

/**
 * Create main table
 * @private
 */
JSONEditor.prototype._createTable = function() {
  var contentOuter = document.createElement("div");
  contentOuter.className = "jsoneditor-content-outer";
  this.contentOuter = contentOuter;

  this.content = document.createElement("div");
  this.content.className = "jsoneditor-content";
  contentOuter.appendChild(this.content);

  this.table = document.createElement("table");
  this.table.className = "jsoneditor-table";
  this.content.appendChild(this.table);

  // IE8 does not handle overflow='auto' correctly.
  // Therefore, set overflow to 'scroll'
  var ieVersion = JSONEditor.getInternetExplorerVersion();
  if (ieVersion == 8) {
    this.content.style.overflow = "scroll";
  }

  // create colgroup where the first two columns don't have a fixed
  // width, and the edit columns do have a fixed width
  var col;
  this.colgroupContent = document.createElement("colgroup");
  col = document.createElement("col");
  col.width = "24px";
  this.colgroupContent.appendChild(col);
  col = document.createElement("col");
  this.colgroupContent.appendChild(col);
  col = document.createElement("col");
  col.width = "24px";
  this.colgroupContent.appendChild(col);
  col = document.createElement("col");
  col.width = "24px";
  this.colgroupContent.appendChild(col);
  col = document.createElement("col");
  col.width = "24px";
  this.colgroupContent.appendChild(col);
  this.table.appendChild(this.colgroupContent);

  this.tbody = document.createElement("tbody");
  this.table.appendChild(this.tbody);

  this.frame.appendChild(contentOuter);
};

/**
 * Find the node from an event target
 * @param {Element} target
 * @return {JSONEditor.Node | undefined} node  or undefined when not found
 */
JSONEditor.getNodeFromTarget = function(target) {
  while (target) {
    if (target.node) {
      return target.node;
    }
    target = target.parentNode;
  }

  return undefined;
};

/**
 * Create a JSONFormatter and attach it to given container
 * @constructor JSONFormatter
 * @param {Element} container
 * @param {Object} [options]         Object with options. available options:
 *                                   {Number} indentation  Number of indentation
 *                                                         spaces. 4 by default.
 *                                   {function} change     Callback method
 *                                                         triggered on change
 * @param {JSON | String} [json]     initial contents of the formatter
 */
JSONFormatter = function(container, options, json) {
  // check availability of JSON parser (not available in IE7 and older)
  if (!JSON) {
    throw new Error(
      "您当前使用的浏览器不支持 JSON. \n\n" +
        "请下载安装最新版本的浏览器, 本站推荐Google Chrome.\n" +
        "(PS: 当前主流浏览器都支持JSON)."
    );
  }

  this.container = container;
  this.indentation = 4; // number of spaces

  this.width = container.clientWidth;
  this.height = container.clientHeight;

  this.frame = document.createElement("div");
  this.frame.className = "jsoneditor-frame";
  this.frame.onclick = function(event) {
    // prevent default submit action when JSONFormatter is located inside a form
    JSONEditor.Events.preventDefault(event);
  };

  // create menu
  this.menu = document.createElement("div");
  this.menu.className = "jsoneditor-menu";
  this.frame.appendChild(this.menu);

  // create format button
  var buttonFormat = document.createElement("button");
  //buttonFormat.innerHTML = 'Format';
  buttonFormat.className = "jsoneditor-menu jsoneditor-format";
  buttonFormat.title = "格式化JSON数据";
  //buttonFormat.className = 'jsoneditor-button';
  this.menu.appendChild(buttonFormat);

  // create compact button
  var buttonCompact = document.createElement("button");
  //buttonCompact.innerHTML = 'Compact';
  buttonCompact.className = "jsoneditor-menu jsoneditor-compact";
  buttonCompact.title = "压缩JSON数据, 清除所有空白字符";
  //buttonCompact.className = 'jsoneditor-button';
  this.menu.appendChild(buttonCompact);

  this.content = document.createElement("div");
  this.content.className = "jsonformatter-content";
  this.frame.appendChild(this.content);

  this.textarea = document.createElement("textarea");
  this.textarea.className = "jsonformatter-textarea";
  this.textarea.spellcheck = false;
  this.content.appendChild(this.textarea);

  var textarea = this.textarea;

  // read the options
  if (options) {
    if (options.change) {
      // register on change event
      if (this.textarea.oninput === null) {
        this.textarea.oninput = function() {
          options.change();
        };
      } else {
        // oninput is undefined. For IE8-
        this.textarea.onchange = function() {
          options.change();
        };
      }
    }
    if (options.indentation) {
      this.indentation = Number(options.indentation);
    }
  }

  var me = this;
  buttonFormat.onclick = function() {
    try {
      var json = JSONEditor.parse(textarea.value);
      textarea.value = JSON.stringify(json, null, me.indentation);
    } catch (err) {
      me.onError(err);
    }
  };
  buttonCompact.onclick = function() {
    try {
      var json = JSONEditor.parse(textarea.value);
      textarea.value = JSON.stringify(json);
    } catch (err) {
      me.onError(err);
    }
  };

  this.container.appendChild(this.frame);

  // load initial json object or string
  if (typeof json == "string") {
    this.setText(json);
  } else {
    this.set(json);
  }
};

/**
 * This method is executed on error.
 * It can be overwritten for each instance of the JSONFormatter
 * @param {String} err
 */
JSONFormatter.prototype.onError = function(err) {
  // action should be implemented for the instance
};

/**
 * Set json data in the formatter
 * @param {Object} json
 */
JSONFormatter.prototype.set = function(json) {
  this.textarea.value = JSON.stringify(json, null, this.indentation);
};

/**
 * Get json data from the formatter
 * @return {Object} json
 */
JSONFormatter.prototype.get = function() {
  return JSONEditor.parse(this.textarea.value);
};

/**
 * Get the text contents of the JSONFormatter
 * @return {String} text
 */
JSONFormatter.prototype.getText = function() {
  return this.textarea.value;
};

/**
 * Set the text contents of the JSONFormatter
 * @param {String} text
 */
JSONFormatter.prototype.setText = function(text) {
  this.textarea.value = text;
};

/**
 * @constructor JSONEditor.SearchBox
 * Create a search box in given HTML container
 * @param {JSONEditor} editor   The JSON Editor to attach to
 * @param {Element} container   HTML container element of where to create the
 *                              search box
 */
JSONEditor.SearchBox = function(editor, container) {
  var searchBox = this;

  this.editor = editor;
  this.timeout = undefined;
  this.delay = 200; // ms
  this.lastText = undefined;

  this.dom = {};
  this.dom.container = container;

  var table = document.createElement("table");
  this.dom.table = table;
  table.className = "jsoneditor-search";
  container.appendChild(table);
  var tbody = document.createElement("tbody");
  this.dom.tbody = tbody;
  table.appendChild(tbody);
  var tr = document.createElement("tr");
  tbody.appendChild(tr);

  var td = document.createElement("td");
  td.className = "jsoneditor-search";
  tr.appendChild(td);
  var results = document.createElement("div");
  this.dom.results = results;
  results.className = "jsoneditor-search-results";
  td.appendChild(results);

  td = document.createElement("td");
  td.className = "jsoneditor-search";
  tr.appendChild(td);
  var divInput = document.createElement("div");
  this.dom.input = divInput;
  divInput.className = "jsoneditor-search";
  divInput.title = "查找区块和值";
  td.appendChild(divInput);

  // table to contain the text input and search button
  var tableInput = document.createElement("table");
  tableInput.className = "jsoneditor-search-input";
  divInput.appendChild(tableInput);
  var tbodySearch = document.createElement("tbody");
  tableInput.appendChild(tbodySearch);
  tr = document.createElement("tr");
  tbodySearch.appendChild(tr);

  var refreshSearch = document.createElement("button");
  refreshSearch.className = "jsoneditor-search-refresh";
  td = document.createElement("td");
  td.appendChild(refreshSearch);
  tr.appendChild(td);

  var search = document.createElement("input");
  this.dom.search = search;
  search.className = "jsoneditor-search";
  search.oninput = function(event) {
    searchBox.onDelayedSearch(event);
  };
  search.onchange = function(event) {
    // For IE 8
    searchBox.onSearch(event);
  };
  search.onkeydown = function(event) {
    searchBox.onKeyDown(event);
  };
  search.onkeyup = function(event) {
    searchBox.onKeyUp(event);
  };
  refreshSearch.onclick = function(event) {
    search.select();
  };

  // TODO: ESC in FF restores the last input, is a FF bug, https://bugzilla.mozilla.org/show_bug.cgi?id=598819
  td = document.createElement("td");
  td.appendChild(search);
  tr.appendChild(td);

  var searchNext = document.createElement("button");
  searchNext.title = "下一个 (Enter)";
  searchNext.className = "jsoneditor-search-next";
  searchNext.onclick = function() {
    searchBox.next();
  };
  td = document.createElement("td");
  td.appendChild(searchNext);
  tr.appendChild(td);

  var searchPrevious = document.createElement("button");
  searchPrevious.title = "上一个 (Shift+Enter)";
  searchPrevious.className = "jsoneditor-search-previous";
  searchPrevious.onclick = function() {
    searchBox.previous();
  };
  td = document.createElement("td");
  td.appendChild(searchPrevious);
  tr.appendChild(td);
};

/**
 * Go to the next search result
 */
JSONEditor.SearchBox.prototype.next = function() {
  if (this.results != undefined) {
    var index = this.resultIndex != undefined ? this.resultIndex + 1 : 0;
    if (index > this.results.length - 1) {
      index = 0;
    }
    this.setActiveResult(index);
  }
};

/**
 * Go to the prevous search result
 */
JSONEditor.SearchBox.prototype.previous = function() {
  if (this.results != undefined) {
    var max = this.results.length - 1;
    var index = this.resultIndex != undefined ? this.resultIndex - 1 : max;
    if (index < 0) {
      index = max;
    }
    this.setActiveResult(index);
  }
};

/**
 * Set new value for the current active result
 * @param {Number} index
 */
JSONEditor.SearchBox.prototype.setActiveResult = function(index) {
  // de-activate current active result
  if (this.activeResult) {
    var prevNode = this.activeResult.node;
    var prevElem = this.activeResult.elem;
    if (prevElem == "field") {
      delete prevNode.searchFieldActive;
    } else {
      delete prevNode.searchValueActive;
    }
    prevNode.updateDom();
  }

  if (!this.results || !this.results[index]) {
    // out of range, set to undefined
    this.resultIndex = undefined;
    this.activeResult = undefined;
    return;
  }

  this.resultIndex = index;

  // set new node active
  var node = this.results[this.resultIndex].node;
  var elem = this.results[this.resultIndex].elem;
  if (elem == "field") {
    node.searchFieldActive = true;
  } else {
    node.searchValueActive = true;
  }
  this.activeResult = this.results[this.resultIndex];
  node.updateDom();

  node.scrollTo();
};

/**
 * Set the focus to the currently active result. If there is no currently
 * active result, the next search result will get focus
 */
JSONEditor.SearchBox.prototype.focusActiveResult = function() {
  if (!this.activeResult) {
    this.next();
  }

  if (this.activeResult) {
    this.activeResult.node.focus(this.activeResult.elem);
  }
};

/**
 * Cancel any running onDelayedSearch.
 */
JSONEditor.SearchBox.prototype.clearDelay = function() {
  if (this.timeout != undefined) {
    clearTimeout(this.timeout);
    delete this.timeout;
  }
};

/**
 * Start a timer to execute a search after a short delay.
 * Used for reducing the number of searches while typing.
 * @param {Event} event
 */
JSONEditor.SearchBox.prototype.onDelayedSearch = function(event) {
  // execute the search after a short delay (reduces the number of
  // search actions while typing in the search text box)
  this.clearDelay();
  var searchBox = this;
  this.timeout = setTimeout(function(event) {
    searchBox.onSearch(event);
  }, this.delay);
};

/**
 * Handle onSearch event
 * @param {Event} event
 * @param {boolean} [forceSearch]  If true, search will be executed again even
 *                                 when the search text is not changed.
 *                                 Default is false.
 */
JSONEditor.SearchBox.prototype.onSearch = function(event, forceSearch) {
  this.clearDelay();

  var value = this.dom.search.value;
  var text = value.length > 0 ? value : undefined;
  if (text != this.lastText || forceSearch) {
    // only search again when changed
    this.lastText = text;
    this.results = this.editor.search(text);
    this.setActiveResult(undefined);

    // display search results
    if (text != undefined) {
      var resultCount = this.results.length;
      switch (resultCount) {
        case 0:
          this.dom.results.innerHTML = "区块/值未找到";
          break;
        default:
          this.dom.results.innerHTML =
            "找到&nbsp;" + resultCount + "&nbsp;个结果";
          break;
      }
    } else {
      this.dom.results.innerHTML = "";
    }
  }
};

/**
 * Handle onKeyDown event in the input box
 * @param {Event} event
 */
JSONEditor.SearchBox.prototype.onKeyDown = function(event) {
  event = event || window.event;
  var keynum = event.which || event.keyCode;
  if (keynum == 27) {
    // ESC
    this.dom.search.value = ""; // clear search
    this.onSearch(event);
    JSONEditor.Events.preventDefault(event);
    JSONEditor.Events.stopPropagation(event);
  } else if (keynum == 13) {
    // Enter
    if (event.ctrlKey) {
      // force to search again
      this.onSearch(event, true);
    } else if (event.shiftKey) {
      // move to the previous search result
      this.previous();
    } else {
      // move to the next search result
      this.next();
    }
    JSONEditor.Events.preventDefault(event);
    JSONEditor.Events.stopPropagation(event);
  }
};

/**
 * Handle onKeyUp event in the input box
 * @param {Event} event
 */
JSONEditor.SearchBox.prototype.onKeyUp = function(event) {
  event = event || window.event;
  var keynum = event.which || event.keyCode;
  if (keynum != 27 && keynum != 13) {
    // !ESC and !Enter
    this.onDelayedSearch(event); // For IE 8
  }
};

// create namespace for event methods
JSONEditor.Events = {};

/**
 * Add and event listener. Works for all browsers
 * @param {Element}     element    An html element
 * @param {string}      action     The action, for example "click",
 *                                 without the prefix "on"
 * @param {function}    listener   The callback function to be executed
 * @param {boolean}     useCapture
 * @return {function}   the created event listener
 */
JSONEditor.Events.addEventListener = function(
  element,
  action,
  listener,
  useCapture
) {
  if (element.addEventListener) {
    if (useCapture === undefined) useCapture = false;

    if (
      action === "mousewheel" &&
      navigator.userAgent.indexOf("Firefox") >= 0
    ) {
      action = "DOMMouseScroll"; // For Firefox
    }

    element.addEventListener(action, listener, useCapture);
    return listener;
  } else {
    // IE browsers
    var f = function() {
      return listener.call(element, window.event);
    };
    element.attachEvent("on" + action, f);
    return f;
  }
};

/**
 * Remove an event listener from an element
 * @param {Element}  element   An html dom element
 * @param {string}   action    The name of the event, for example "mousedown"
 * @param {function} listener  The listener function
 * @param {boolean}  useCapture
 */
JSONEditor.Events.removeEventListener = function(
  element,
  action,
  listener,
  useCapture
) {
  if (element.removeEventListener) {
    // non-IE browsers
    if (useCapture === undefined) useCapture = false;

    if (
      action === "mousewheel" &&
      navigator.userAgent.indexOf("Firefox") >= 0
    ) {
      action = "DOMMouseScroll"; // For Firefox
    }

    element.removeEventListener(action, listener, useCapture);
  } else {
    // IE browsers
    element.detachEvent("on" + action, listener);
  }
};

/**
 * Stop event propagation
 * @param {Event} event
 */
JSONEditor.Events.stopPropagation = function(event) {
  if (!event) event = window.event;

  if (event.stopPropagation) {
    event.stopPropagation(); // non-IE browsers
  } else {
    event.cancelBubble = true; // IE browsers
  }
};

/**
 * Cancels the event if it is cancelable, without stopping further propagation of the event.
 * @param {Event} event
 */
JSONEditor.Events.preventDefault = function(event) {
  if (!event) event = window.event;

  if (event.preventDefault) {
    event.preventDefault(); // non-IE browsers
  } else {
    event.returnValue = false; // IE browsers
  }
};

/**
 * Retrieve the absolute left value of a DOM element
 * @param {Element} elem    A dom element, for example a div
 * @return {Number} left    The absolute left position of this element
 *                          in the browser page.
 */
JSONEditor.getAbsoluteLeft = function(elem) {
  var left = 0;
  var body = document.body;
  while (elem != null && elem != body) {
    left += elem.offsetLeft;
    left -= elem.scrollLeft;
    elem = elem.offsetParent;
  }
  return left;
};

/**
 * Retrieve the absolute top value of a DOM element
 * @param {Element} elem    A dom element, for example a div
 * @return {Number} top    The absolute top position of this element
 *                          in the browser page.
 */
JSONEditor.getAbsoluteTop = function(elem) {
  var top = 0;
  var body = document.body;
  while (elem != null && elem != body) {
    top += elem.offsetTop;
    top -= elem.scrollTop;
    elem = elem.offsetParent;
  }
  return top;
};

/**
 * add a className to the given elements style
 * @param {Element} elem
 * @param {String} className
 */
JSONEditor.addClassName = function(elem, className) {
  var classes = elem.className.split(" ");
  if (classes.indexOf(className) == -1) {
    classes.push(className); // add the class to the array
    elem.className = classes.join(" ");
  }
};

/**
 * add a className to the given elements style
 * @param {Element} elem
 * @param {String} className
 */
JSONEditor.removeClassName = function(elem, className) {
  var classes = elem.className.split(" ");
  var index = classes.indexOf(className);
  if (index != -1) {
    classes.splice(index, 1); // remove the class from the array
    elem.className = classes.join(" ");
  }
};

/**
 * Strip the formatting from the contents of a div
 * the formatting from the div itself is not stripped, only from its childs.
 * @param {Element} divElement
 */
JSONEditor.stripFormatting = function(divElement) {
  var childs = divElement.childNodes;
  for (var i = 0, iMax = childs.length; i < iMax; i++) {
    var child = childs[i];

    // remove the style
    if (child.style) {
      // TODO: test if child.attributes does contain style
      child.removeAttribute("style");
    }

    // remove all attributes
    var attributes = child.attributes;
    if (attributes) {
      for (var j = attributes.length - 1; j >= 0; j--) {
        var attribute = attributes[j];
        if (attribute.specified == true) {
          child.removeAttribute(attribute.name);
        }
      }
    }

    // recursively strip childs
    JSONEditor.stripFormatting(child);
  }
};

/**
 * Set focus to the end of an editable div
 * code from Nico Burns
 * http://stackoverflow.com/users/140293/nico-burns
 * http://stackoverflow.com/questions/1125292/how-to-move-cursor-to-end-of-contenteditable-entity
 * @param {Element} contentEditableElement
 */
JSONEditor.setEndOfContentEditable = function(contentEditableElement) {
  var range, selection;
  if (document.createRange) {
    //Firefox, Chrome, Opera, Safari, IE 9+
    range = document.createRange(); //Create a range (a range is a like the selection but invisible)
    range.selectNodeContents(contentEditableElement); //Select the entire contents of the element with the range
    range.collapse(false); //collapse the range to the end point. false means collapse to end rather than the start
    selection = window.getSelection(); //get the selection object (allows you to change selection)
    selection.removeAllRanges(); //remove any selections already made
    selection.addRange(range); //make the range you have just created the visible selection
  } else if (document.selection) {
    //IE 8 and lower
    range = document.body.createTextRange(); //Create a range (a range is a like the selection but invisible)
    range.moveToElementText(contentEditableElement); //Select the entire contents of the element with the range
    range.collapse(false); //collapse the range to the end point. false means collapse to end rather than the start
    range.select(); //Select the range (make it the visible selection
  }
};

/**
 * Get the inner text of an HTML element (for example a div element)
 * @param {Element} element
 * @param {Object} [buffer]
 * @return {String} innerText
 */
JSONEditor.getInnerText = function(element, buffer) {
  var first = buffer == undefined;
  if (first) {
    buffer = {
      text: "",
      flush: function() {
        var text = this.text;
        this.text = "";
        return text;
      },
      set: function(text) {
        this.text = text;
      }
    };
  }

  // text node
  if (element.nodeValue) {
    return buffer.flush() + element.nodeValue;
  }

  // divs or other HTML elements
  if (element.hasChildNodes()) {
    var childNodes = element.childNodes;
    var innerText = "";

    for (var i = 0, iMax = childNodes.length; i < iMax; i++) {
      var child = childNodes[i];

      if (child.nodeName == "DIV" || child.nodeName == "P") {
        var prevChild = childNodes[i - 1];
        var prevName = prevChild ? prevChild.nodeName : undefined;
        if (
          prevName &&
          prevName != "DIV" &&
          prevName != "P" &&
          prevName != "BR"
        ) {
          innerText += "\n";
          buffer.flush();
        }
        innerText += JSONEditor.getInnerText(child, buffer);
        buffer.set("\n");
      } else if (child.nodeName == "BR") {
        innerText += buffer.flush();
        buffer.set("\n");
      } else {
        innerText += JSONEditor.getInnerText(child, buffer);
      }
    }

    return innerText;
  } else {
    if (
      element.nodeName == "P" &&
      JSONEditor.getInternetExplorerVersion() != -1
    ) {
      // On Internet Explorer, a <p> with hasChildNodes()==false is
      // rendered with a new line. Note that a <p> with
      // hasChildNodes()==true is rendered without a new line
      // Other browsers always ensure there is a <br> inside the <p>,
      // and if not, the <p> does not render a new line
      return buffer.flush();
    }
  }

  // br or unknown
  return "";
};

/**
 * Returns the version of Internet Explorer or a -1
 * (indicating the use of another browser).
 * Source: http://msdn.microsoft.com/en-us/library/ms537509(v=vs.85).aspx
 * @return {Number} Internet Explorer version, or -1 in case of an other browser
 */
JSONEditor._ieVersion = undefined;
JSONEditor.getInternetExplorerVersion = function() {
  if (JSONEditor._ieVersion == undefined) {
    var rv = -1; // Return value assumes failure.
    if (navigator.appName == "Microsoft Internet Explorer") {
      var ua = navigator.userAgent;
      var re = new RegExp("MSIE ([0-9]{1,}[.0-9]{0,})");
      if (re.exec(ua) != null) {
        rv = parseFloat(RegExp.$1);
      }
    }

    JSONEditor._ieVersion = rv;
  }

  return JSONEditor._ieVersion;
};

JSONEditor.ieVersion = JSONEditor.getInternetExplorerVersion();

/**
 * Parse JSON using the parser built-in in the browser.
 * On exception, the jsonString is validated and a detailed error is thrown.
 * @param {String} jsonString
 */
JSONEditor.parse = function(jsonString) {
  try {
    return JSON.parse(jsonString);
  } catch (err) {
    // get a detailed error message using validate
    var message = JSONEditor.validate(jsonString) || err;
    throw new Error(message);
  }
};

/**
 * Validate a string containing a JSON object
 * This method uses JSONLint to validate the String. If JSONLint is not
 * available, the built-in JSON parser of the browser is used.
 * @param {String} jsonString   String with an (invalid) JSON object
 * @return {String | undefined} Returns undefined when the string is valid JSON,
 *                              returns a string with an error message when
 *                              the data is invalid
 */
JSONEditor.validate = function(jsonString) {
  var message = undefined;

  try {
    if (window.jsonlint) {
      window.jsonlint.parse(jsonString);
    } else {
      JSON.parse(jsonString);
    }
  } catch (err) {
    message = '<pre class="error">' + err.toString() + "</pre>";
    if (window.jsonlint) {
      message +=
        '<div id="by-jsonlint">由 <a class="error" href="http://zaach.github.com/jsonlint/" target="_blank">' +
        "JSONLint" +
        "</a> 提供验证.</div>";
    }
  }

  return message;
};

function jsonArea(ob) {
  let inputEle = ob.el;
  let insert = ob.insert;
  let nth = ob.nth;
  let change = ob.change;
  let thisare = new Object();
  if (!inputEle) {
    throw new Error("没有提供数据源.");
  }
  // 如果选择器指向多个元素
  if (nth) {
    thisare.data = document.querySelectorAll(inputEle)[nth - 1];
  } else {
    let all = document.querySelectorAll(inputEle);
    for (var i = 0; i < all.length; i++) {
      jsonArea({ el: inputEle, insert: insert, nth: i + 1, change: change });
    }
    // thisare.data = document.querySelector(inputEle);
    return;
  }
  // 创建容器
  thisare.container = document.createElement("div");
  thisare.container.style.width = "100%";
  thisare.container.style.height = "auto";
  thisare.data.parentElement.insertBefore(thisare.container, thisare.data);
  // 判断字符串是否为json
  thisare.isJson = function(str) {
    if (!isNaN(str)) return false;
    if (str == "") return false;
    if (str == '""') return false;
    if (typeof str == "string") {
      try {
        JSON.parse(str);
        return true;
      } catch (e) {
        //	            console.log(e);
        return false;
      }
    }
    console.log("不是一个stringify");
  };
  // json数据
  thisare.jsonval = "{}";
  // 编辑过程中改变后触发
  thisare.syn = function() {};
  // 判断是 textarea还是input
  if (thisare.data.tagName == "TEXTAREA") {
    thisare.jsonval = thisare.data.innerHTML;
    thisare.syn = function() {
      thisare.data.innerHTML = JSON.stringify(thisare.jsonEditor.get());
      change(thisare.jsonEditor.get());
    };
  } else if (thisare.data.tagName == "INPUT") {
    thisare.jsonval = thisare.data.value;
    thisare.syn = function() {
      thisare.data.value = JSON.stringify(thisare.jsonEditor.get());
      change(thisare.jsonEditor.get());
    };
  }
  // 里面不是json,并且不是插入，就不成为json编辑器
  thisare.hashJson = thisare.isJson(thisare.jsonval);
  if (!thisare.hashJson && !insert) return;
  thisare.data.style.display = "none";
  // 为容器创建编辑器
  thisare.jsonEditor = new JSONEditor(thisare.container, {
    change: function() {
      thisare.lastChanged = thisare.jsonEditor;
      thisare.syn();
    }
  });
  if (thisare.hashJson) {
    thisare.jsonEditor.set(JSON.parse(thisare.jsonval));
  }
  console.log("in jsonArea");
  return thisare;
}
